package org.apache.felix.dm.impl.dependencies.bi;

import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.felix.dm.Component;
import org.apache.felix.dm.Dependency;
import org.apache.felix.dm.DependencyActivation;
import org.apache.felix.dm.DependencyManager;
import org.apache.felix.dm.DependencyService;

@SuppressWarnings("rawtypes")
public class CallbackDependency implements Dependency, DependencyActivation, Runnable {
	private final Toggle m_toggle;

	private final List<DependencyService> m_services = new ArrayList<DependencyService>();

	private final String triggerMethod;

	private boolean instanceBound = false;

	private boolean persistent = false;

	public CallbackDependency(Toggle toggle, String targetMethod) {
		this.m_toggle = toggle;
		this.triggerMethod = targetMethod;
	}

	public CallbackDependency(Toggle toggle, String targetMethod, boolean instanceBound, boolean persistent) {
		this.m_toggle = toggle;
		this.triggerMethod = targetMethod;
		this.instanceBound = instanceBound;
		this.persistent  = persistent;
	}

	/**
	 * Add a special Controllable dependency to a component. A controllable dependency is controlled though intercepting specific method invocations.
	 * These method invocations trigger the availability of the controllable dependency. The dependency is marked as available after the specified method
	 * has been invoked.
	 * 
	 * @param manager dependency manager
	 * @param targetService the implementation object for the component
	 * @param serviceNames the primary business interfaces for the component
	 * @param properties the service properties the component should be registered with
	 * @param capabilityInterfaces the capability interfaces used to drive the controllable dependency
	 * @param triggerMethods the trigger methods for the capability interfaces (one per interface) which trigger the availability of the dependency.
	 * @param additionalDependencies the depencies for the primary business interfaces for the component (serviceNames parameter)
	 * @return
	 */
	public static ComponentWithCapability createComponentWithCapability(DependencyManager manager, Object targetService, String[] serviceNames, Dictionary properties,
			Class<?>[] capabilityInterfaces, String[] triggerMethods, Dependency[] additionalDependencies) {
		Component component = manager.createComponent().setInterface(serviceNames, properties).setImplementation(targetService);
		return createComponentAndProxy(targetService, capabilityInterfaces, triggerMethods, component, additionalDependencies);
	}

	public static ComponentWithCapability createComponent(DependencyManager manager, Object targetService, String[] serviceNames, Dictionary properties,
			Class<?>[] capabilityInterfaces, String[] triggerMethods) {
		Component component = manager.createComponent().setInterface(serviceNames, properties).setImplementation(targetService);
		return createComponentAndProxy(targetService, capabilityInterfaces, triggerMethods, component, null);
	}

	/**
	 * Add a special Controllable dependency to a component. A controllable dependency is controlled though intercepting specific method invocations.
	 * These method invocations trigger the availability of the controllable dependency. The dependency is marked as available after the specified method
	 * has been invoked.
	 * 
	 * @param manager dependency manager
	 * @param targetService the implementation object for the component
	 * @param serviceInterface the interface for the component to be adapted
	 * @param filter the filter
	 * @param capabilityInterfaces the capability interfaces used to drive the controllable dependency
	 * @param triggerMethods the trigger methods for the capability interfaces (one per interface) which trigger the availability of the dependency.
	 * @return
	 */
	public static CallbackDependencyAdapterBuilder createAdapter(DependencyManager manager, Class<?> targetServiceClass, Class<?> serviceInterface,
			String filter, String[] serviceNames, Dictionary properties, Class<?>[] capabilityInterfaces, String[] triggerMethods) {
		AdapterCallbackHandler callbackHandler = new AdapterCallbackHandler(manager, capabilityInterfaces, triggerMethods);
		Component component = manager.createAdapterService(serviceInterface, filter).setInterface(serviceNames, properties)
				.setImplementation(targetServiceClass).setCallbacks(callbackHandler, "init", "start", "stop", "destroy");
		return new CallbackDependencyAdapterBuilder(component, callbackHandler);
	}

	/**
	 * Add a special Controllable dependency to a component. A controllable dependency is controlled though intercepting specific method invocations.
	 * These method invocations trigger the availability of the controllable dependency. The dependency is marked as available after the specified method
	 * has been invoked.
	 * 
	 * @param manager dependency manager
	 * @param targetService the implementation object for the component
	 * @param serviceInterface the interface for the component to be adapted
	 * @param addedServiceCallback callback added when component to be adapted is added
	 * @param changedServiceCallback callback added when component to be adapted is changed
	 * @param removedServiceCallback callback added when component to be adapted is removed
	 * @param filter the filter
	 * @param capabilityInterfaces the capability interfaces used to drive the controllable dependency
	 * @param triggerMethods the trigger methods for the capability interfaces (one per interface) which trigger the availability of the dependency.
	 * @return
	 */
	public static CallbackDependencyAdapterBuilder createAdapter(DependencyManager manager, Class<?> targetServiceClass, Class<?> serviceInterface,
			String addedServiceCallback, String changedServiceCallback, String removedServiceCallback, String filter, String[] serviceNames, Dictionary properties,
			Class<?>[] capabilityInterfaces, String[] triggerMethods) {
		AdapterCallbackHandler callbackHandler = new AdapterCallbackHandler(manager, capabilityInterfaces, triggerMethods);
		Component component = manager.createAdapterService(serviceInterface, filter, addedServiceCallback, changedServiceCallback, removedServiceCallback)
				.setInterface(serviceNames, properties).setImplementation(targetServiceClass).setCallbacks(callbackHandler, "init", "start", "stop", "destroy");
		return new CallbackDependencyAdapterBuilder(component, callbackHandler);
	}
	
	public static CallbackDependencyAdapterBuilder createResourceAdapter(DependencyManager manager, Class<?> adapterClass, String filter, String[] serviceNames, Object propertyFactory, Class<?>[] capabilityInterfaces, String[] triggerMethods, Object callbackInstance) {
		AdapterCallbackHandler callbackHandler = new AdapterCallbackHandler(manager, capabilityInterfaces, triggerMethods, callbackInstance);
		Component component = manager.createResourceAdapterService(filter, propertyFactory, "getResourceProperties", callbackHandler, "changed")
			.setImplementation(adapterClass)
			.setCallbacks(callbackHandler, "init", "start", "stop", "destroy");
		return new CallbackDependencyAdapterBuilder(component, callbackHandler);
	}

	private static ComponentWithCapability createComponentAndProxy(Object targetService, Class[] capabilityInterfaces, String[] triggerMethods, Component component,
			Dependency[] additionalDependencies) {
		List<Dependency> dependencies = new ArrayList<Dependency>();
		if (additionalDependencies != null) {
			dependencies.addAll(Arrays.asList(additionalDependencies));
		}
		Map<String, Toggle> toggles = new HashMap<String, Toggle>();
		int pos = 0;
		for (String targetMethod : triggerMethods) {
			Toggle toggle = new Toggle();
			toggles.put(targetMethod, toggle);
			dependencies.add(new CallbackDependency(toggle, targetMethod));
			pos++;
		}
		component.add(dependencies);
		Object implementation = Proxy.newProxyInstance(targetService.getClass().getClassLoader(), capabilityInterfaces,
				new ServiceProxy(targetService, toggles));
		return new ComponentWithCapability(component, implementation);
	}

	public Dependency createCopy() {
		return new CallbackDependency(m_toggle, triggerMethod);
	}

	public Object getAutoConfigInstance() {
		return "" + m_toggle.isAvailable();
	}

	public String getAutoConfigName() {
		return null;
	}

	public Class<?> getAutoConfigType() {
		return String.class;
	}

	public Dictionary getProperties() {
		return null;
	}

	public void run() {
		// invoked on every change
		if (m_toggle.isAvailable()) {
			Object[] services = m_services.toArray();
			for (int i = 0; i < services.length; i++) {
				DependencyService ds = (DependencyService) services[i];
				ds.dependencyAvailable(this);
				if (!isRequired()) {
					invokeAdded(ds);
				}
			}
		} else {
			Object[] services = m_services.toArray();
			for (int i = 0; i < services.length; i++) {
				DependencyService ds = (DependencyService) services[i];
				ds.dependencyUnavailable(this);
				if (!isRequired()) {
					invokeRemoved(ds);
				}
			}
		}
	}

	public void invokeAdded(DependencyService service) {
		invoke(service, "added");
	}

	public void invokeRemoved(DependencyService service) {
		invoke(service, "removed");
	}

	public void invoke(DependencyService dependencyService, String name) {
		if (name != null) {
			dependencyService.invokeCallbackMethod(getCallbackInstances(dependencyService), name, new Class[][] { { String.class }, { Object.class }, {} },
					new Object[][] { { getAutoConfigInstance() }, { getAutoConfigInstance() }, {} });
		}
	}

	private synchronized Object[] getCallbackInstances(DependencyService dependencyService) {
		return dependencyService.getCompositionInstances();
	}

	public boolean isAutoConfig() {
		return false;
	}

	public boolean isAvailable() {
		return m_toggle.isAvailable();
	}

	public boolean isInstanceBound() {
		return instanceBound;
	}

	public boolean isPropagated() {
		return false;
	}

	public boolean isRequired() {
		return true;
	}

	public void start(DependencyService service) {
		synchronized (this) {
			m_services.add(service);
		}
		m_toggle.setRunnable(this);
	}

	public void stop(DependencyService service) {
		synchronized (this) {
			m_services.remove(service);
		}
		m_toggle.setRunnable(null);
	}
	
	public boolean isPersistent() {
		return persistent;
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("CallbackDependency [method=");
		builder.append(triggerMethod);
		builder.append("]");
		return builder.toString();
	}

}
